В духе продолжения складывающейся традиции парных постов, я вслед за Максом Ищенко просто должен написать про "паджинацию" в Django.

Выборка

В Django уже есть небольшой объектик для постраничного вывода результатов. Чтобы его использовать, надо скормить ему табличку, из которой брать данные и, если надо, дополнительные параметры выборки. Покажу, как это выглядит, на примере все того же "Некого Музыкального Сервиса", про черновой стайлинг которого я недавно писал. Задача простая: вывести постраничный список альбомов, принадлежащих конкретному пользователю.

from django.models.main import albums
from django.core.paginator import ObjectPaginator

def albums(request):

  # Создается объект
  paginator = ObjectPaginator(
    albums,                               # что выбирать
    {'owner__id__exact':request.user.id}, # условие
    20)                                   # сколько на страницу

  # Выбирается нужная страница по номеру из GET-параметров
  page = int(request.GET.get('page','1'))
  album_list = paginator.get_page(page-1)

  # Выборка, паджинатор и номер сраницы отправляются в шаблон
  return render_to_response('albums',{
      'albums':album_list,
      'paginator':paginator,
      'page':page,
    },context_instance=DjangoContext(request))

Немного комментариев:

Если вы не пишите обработчик запроса вручную, а пользуетесь generic views, то там тоже можно задать постраничный вывод:

(r'^user/albums/$',
  'django.views.generic.list_detail.object_list',
  {'app_label':'myapp','module_name':'albums','paginate_by':20})

В шаблоне будет доступна куча переменных с текущей страницей, общим количеством страниц, соседними страницами и т.п. Перечислять не буду, они все есть в документации.

Оформление

Выдать объекты в шаблон — это еще не все. Я все время повторяю один из принципов Django, что он старается не навязывать никакого определенного HTML кода. В случае с постраничным выводом это особенно правильно, потому что вариантов нарисовать постраничную навигацию — множество.

Делать это я обязательно рекомендую не в самом шаблоне, а в виде отдельного шаблонного тега, потому что вы будете использовать это в очень многих местах. А чтобы слова "сделать свой тег" не отпугивали, я напишу его для вас прямо здесь :-).

Сначала надо сделать в директории приложения директорию "templatetags", в которую поместить два файла:

Для НМС я написал пока очень базовый тег, который умеет только выводить "n стр. из m" и ссылки на соседние страницы, если они есть. Вот весь код:

@register.simple_tag(takes_context=True)
def paginator(context):

  paginator = context['paginator']
  page = context['page']

  # Делаются линки на соседние страницы, если они есть
  if paginator.has_next_page(page-1):
    next_link = '<a href="?page=%s" class="next">вперед →</a>'%page+1
  else:
    next_link = ''
  if paginator.has_previous_page(page-1):
    previous_link = '<a href="?page=%s" class="previous">← назад</a> '%page-1
  else:
    previous_link = ''

  # Составляется финальный код
  return '''
    <div class="paginator">
      %s
      <span class="pages">стр. %s из %s</span>
      %s
    </div>'''%(previous_link, page, paginator.pages, next_link)

И вот так он вызывается в шаблоне:

{% paginator %}

Необходимые комментарии:

На самом деле, реальный тег в проекте все таки не такой простой. Мне понадобилось сделать два варианта вывода страниц: обычный и для архивов, где понятия "вперед" и "назад", как известно, прихотливо меняются. Кому интересно, можно взять файлик "paginator_tags.zip" и использовать в качестве основы в своем проекте.

Пустые списки

У Django'вского паджинатора есть одна неочевидная особенность: когда в выборке объектов нет данных, он не отдает пустой список, а выкидывает некрасивую ошибку. Это довольно неприятно, потому что при отладке у вас обычно есть какой-то набор тестовых данных, и с ним все работает хорошо. Но как только код попадает в другое место, где данных еще нет, там оно все и вылезает :-).

Обходится это просто, прямо в том месте, где идет выборка данных:

from django.core.paginator import ObjectPaginator, InvalidPage
...
try:
  album_list = paginator.get_page(page-1)
except InvalidPage:
  album_list = []

Это можно при желании даже в отдельную функцию завернуть.

Комментарии: 10

  1. dp_wiz

    Спасибо за разъяснения на пальцах. Ваши пальцы нам очень нужны (:

  2. dp_wiz

    Да, paginator у turbogears монструозный.. (=

  3. Лёхха

    Иван, продемострируйте пожалуйста линки, которые генерирует этот паджинатор. Просто первый раз встречаюсь с этим "термином" :) По-сути, если я понял, что-о типа index.html?page=123

    Скоро попробую написать статью как раз про нумерацию страниц. Уже мыслей куча :)

  4. Иван Сагалаев

    Ну сам этот объектик линки не генерит, он нужен для того, чтобы выбирать нужную страницу из базы и предоставлять удобные функции типа "сколько всего страниц", "есть ли следующая/предыдущая страница".

    Как организовывать линки — это полностью зависит от программиста. Сейчас у меня они выглядят как "/user/albums/?page=2". Но можно, например, сделать "/user/albums/pages/2/" или "/user/albums,(page_2)". По-разному, в общем, больное воображение проявлять можно :-)

  5. Лёхха

    ну вот я и вижу, что ?page=2. потому что get :)

    эх, так чешутся руки статейку написать!! вот только сессию закрою и пойду :)

  6. Mkdir

    Иван, спасибо за Ваши статьи о Django! Именно благодаря им я открыл для себя этот крутейший фреймворк.
    Django - шикарнейшая вещь!!! Прошёл туториал, что на сайте djangoproject, и это было для меня прозрением, которое в корне изменило моё видение удобных фреймворков ;-)

    После знакомства с Django, понимаешь что PHP - это игрушка для детей дошкольного возраста :-D Шучу, конечно. Наша команда писала на PHP сложные системы разного направления, но вопрос заключается в том, какой ценой удавались такие вещи как динамическая маршрутизация для IP-телефонии или intelligent server для управления DNS :)

    Ещё раз спасибо Вам за агитацию! =)

  7. Иван Сагалаев

    Оказывается, я поспешил с примером кода шаблонного тега, не проверив его. Вот это место:

    @register.simple_tag(takes_context=True)
    

    ... не работает. Я его увидел в Traс'е, но вот в код это так и не включили. Так что, "simple_tag" остается действительно очень простым. Если в теге нужен контекст, то придется рисовать классы (что муторно).

    P.S. Но вот файлик paginator_tags.zip работает.

  8. cadmi

    Mkdir, а теперь откройте для себя TurboGears :)

  9. dp_wiz

    А потом закройте обратно и больше не вспоминайте. q:

  10. vugar

    Спасибо за интересный пост! Только начинаю осваивать Django, для меня в нем все ново, но уже успел оценить его очаровательную элегантность и продуманность.

    Пытался реализовать твой пример с Paginator на практике, натолкнулся на несколько непонятных моментов. Но ничего, осилил. Вот, рассказываю:

    1. Создаю директорию templatetags в директории моего приложения
    2. Создаю пустой файл с именем __init__.py в директории templatetags
    3. Сам файл с исходником Paginator'а называю, к примеру, app_custom.py и кидаю туда же. Вот этот файл(чуточку измененный файл Ивана):

      from django.template import Library, Node

      register = Library()

      class PaginatorNode(Node):

      def render(self,context): paginator=context['paginator'] page_number=context['page'] class_name='paginator' next_text='Next' previous_text='Previous' if paginator.has_next_page(page_number-1): next_link='%s '%(page_number+1,next_text) else: next_link='' pages_text='page %s of %s '%(page_number,paginator.pages) if paginator.has_previous_page(page_number-1): previous_link='%s '%(page_number-1,previous_text) else: previous_link='' return '%s %s %s'%(class_name,previous_link,pages_text,next_link)

      @register.tag def paginator(parser, token): return PaginatorNode() paginator = register.tag(paginator)

    Где-то в начале template'a указываю...

    {% load bank_extras %}
    

    ...и в нужном месте template'a пишу

    {% paginator %}
    

    После этого у меня все заработало.
    Еще раз спасибо за полезную наводку!

Добавить комментарий